package som;

import java.awt.Point;
import java.util.LinkedList;
import java.util.List;

import main.MainApp;

/**
 * Class representing the board on which the cells of the SOM are placed
 */
public class Board {
	
	private int width, height; // board's dimensions
	private Cell[][] matrix; // holds the SOM's cells	
	
	/**
	 * Constructor- initializes a new board in given dimensions.
	 * Each cell is initialized with a random example
	 * @param w board's width
	 * @param h board's height
	 */
	public Board(int w, int h) 
	{
		// allocate matrix to hold the cells
		width = w;
		height = h;
		matrix = new Cell[width][height];
		
		// initialize each cell with a random example
		for (int row=0; row < height; row++) 
		{
			for (int column=0; column < width; column++)
			{
				Cell newCell = new Cell(row,column,this);
				newCell.setRepresentingExample(Sample.generateSample(MainApp.SAMPLE_LEN));
				matrix[row][column] = newCell;
			}
		}
	}
	
	/**
	 * Finds the SOM's cell that its sample is the most similar
	 * to given sample
	 * @param inputSample sample to find the cell for
	 * @return best matching cell for given sample
	 */
	public Cell getBestMatchingCell(Sample inputSample) {

		Cell bestMatchingCell = matrix[0][0];
		double bestDist = inputSample.getDistance(bestMatchingCell.getRepresentingExample());
		double curDist;
		
		// scan the SOM and find most similar cell
		for (int row = 0; row < height; row++)
		{
			for (int column = 0; column < width; column++)
			{
				// calculate distance to current cell
				curDist = inputSample.getDistance(matrix[row][column].getRepresentingExample());
				
				// update best match
				if (curDist < bestDist) {
					bestMatchingCell = matrix[row][column];
					bestDist = curDist;
				}
			}
		}
		
		// return the best match found
		return bestMatchingCell;
	}
	
	/**
	 * Returns cell matching given position
	 * @param row row index
	 * @param column column index
	 * @return cell matching given position
	 */
	public Cell getCell(int row, int column) {
		return matrix[row][column];
	}

	/**
	 * Returns all neighboring cells of given cell, according to given distance.
	 * A neighbor of distance d is a cell that lays on a perimeter in width of
	 * d steps from the center cell (no wrap-around)
	 * @param dist how wide the neighborhood should be
	 * @param centerCell the cell in the center of the neighborhood
	 * @return all cells in indicated neighborhood
	 */
	public List<Cell> getNeighborhoods(int dist, Cell centerCell)
	{
		// initialize return value
		List<Cell> result = new LinkedList<Cell>();
		
		// get center cell's position
		int cellRow = centerCell.getRow();
		int cellCol = centerCell.getColumn();
		
		// find all cells that their position matches the indicated perimeter
		for (int row = 0; row < height; row++)
		{
			for (int column = 0; column < width; column++)
			{
				if((Math.abs(row-cellRow) == dist && Math.abs(column-cellCol) <= dist) ||
					(Math.abs(column-cellCol) == dist && Math.abs(row-cellRow) <= dist))
				{
					// found such cell, add it to the return value
					result.add(matrix[row][column]);
				}
			}
		}
		
		// return the neighborhood
		return result;
	}
	
	/**
	 * Returns a matrix where its [i,j]'th entry holds the classifications
	 * of all train examples assigned to the [i,j]'th cell in the SOM
	 * @return matrix displaying the classifications of the assigned train example in the SOM
	 */
	public String[][] getClassifications()
	{
		String[][] result = new String[height][width];
		for (int row = 0; row < height; row++)
		{
			for (int column = 0; column < width; column++)
			{
				result[row][column] = matrix[row][column].getClassificationsOfAssignedSamples();
			}
		}
		return result;
	}

	/**
	 * Calculates the quantization error of the SOM:
	 * sums the distance between each train example and its best matching cell 
	 * @return quantization error of the SOM
	 */
	public double quantError() {
		
		double totalError = 0.0; // holds total quantization error
		
		// for each cell
		for (int row = 0 ; row < height ; ++row) {
			for (int col = 0 ; col < width ; ++col) {
				
				// for each of current cell's assigned samples
				Cell currCell = getCell(row, col);
				Sample representingSample = currCell.getRepresentingExample();
				List<Sample> samples = currCell.getAssignedSamples();
				for (Sample currSample : samples) {
					
					// sum distance between current sample and cell's representing sample
					totalError += representingSample.getDistance(currSample);
				}
			}
		}
		
		// return calculated error
		return totalError;
	}
	
	/**
	 * Clears the samples being assigned for each cell 
	 */
	public void clearCells() {
		
		// for each cell
		for (int row = 0 ; row < height ; ++row) {
			for (int col = 0 ; col < width ; ++col) {
				
				// clear current cell's samples
				Cell currCell = getCell(row, col); 
				currCell.getExamplesClassifications().clear();
				currCell.getAssignedSamples().clear();
			}
		}
	}
	
	/**
	 * Calculates the topological error of the SOM:
	 * for each train example, sums the distance between its best matching cell
	 * and its second best matching cell
	 * @return topological error of the SOM
	 */
	public double topologicalError() {
		
		double totalError = 0.0; // holds total topological error		
		
		// for each cell
		for (int row = 0; row < height; ++row) {
			for (int col = 0; col < width; ++col) {
				
				// for each sample assigned to current cell
				List<Sample> assignedSamples = getCell(row, col).getAssignedSamples();
				for (Sample currSample : assignedSamples) {

					// find second most matching cell
					Point bestMatchPos = new Point(row,col);
					Cell secondBestMatch = getSecondBestMatchingCell(bestMatchPos, currSample);
					
					// sum distance between best match and second best match
					totalError += bestMatchPos.distance(secondBestMatch.getRow(), secondBestMatch.getColumn());
				}
			}
		}

		// return calculated error
		return totalError;
	}
	
	/**
	 * Finds second best matching cell for given sample
	 * @param bestMatchPos position of given sample's best matching cell
	 * @param inputSample the sample for which the second best matching cell is found
	 * @return best matching cell for given sample
	 */
	private Cell getSecondBestMatchingCell(Point bestMatchPos, Sample inputSample) {

		// initialize with an arbitrary value
		Cell secondBestMatchingCell = (bestMatchPos.x == 0) ? matrix[1][0] : matrix[0][0];
		double secondBestDist = inputSample.getDistance(secondBestMatchingCell.getRepresentingExample());
		double curDist;

		// scan all cells and find second best matching cell
		for (int row = 0; row < height; ++row) {
			for (int col = 0; col < width; ++col) {

				// skip best matching cell
				if (row == bestMatchPos.x && col == bestMatchPos.y) {
					continue;
				}

				// update distance to second best matching cell
				curDist = inputSample.getDistance(matrix[row][col].getRepresentingExample());
				if (curDist < secondBestDist) {
					secondBestMatchingCell = matrix[row][col];
					secondBestDist = curDist;
				}
			}
		}
		
		// return cell found
		return secondBestMatchingCell;
	}
	
	/**
	 * Returns board's width
	 * @return board's width
	 */
	public int getWidth() {
		return width;
	}
	
	/**
	 * Returns board's height
	 * @return board's height
	 */
	public int getHeight() {
		return height;
	}
}
